<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>The Tetris game in IronPython</title>
<link rel="stylesheet" href="/cfg/format.css" type="text/css">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="keywords" content="IronPython, Winforms, the Tetris game, programming, Linux, games">
<meta name="description" content="The Tetris game in IronPython">
<meta name="language" content="en">
<meta name="author" content="Jan Bodnar">
<meta name="distribution" content="global">

<script type="text/javascript" src="/lib/jquery.js"></script>
<script type="text/javascript" src="/lib/common.js"></script>

</head>

<body>

<div class="container">

<div id="wide_ad" class="ltow">
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* 160x600, August 2011 */
google_ad_slot = "2484182563";
google_ad_width = 160;
google_ad_height = 600;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>

<div class="content">


<a href="/" title="Home">Home</a>&nbsp;
<a href="..">Contents</a>


<h1>The Tetris game in IronPython</h1>



<h2>Tetris</h2>

<p>
The tetris game is one of the most popular computer games ever created. 
The original game was designed and programmed
by a russian programmer <b>Alexey Pajitnov</b> in 1985. Since then, 
Tetris is available on almost every computer platform in lots of variations. 
Even my mobile phone has a modified version of the tetris game. 
</p>

<div class="center">
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* horizontal */
google_ad_slot = "1734478269";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>

<p>
Tetris is called a falling block puzzle game. In this game, we have seven 
different shapes called <b>tetrominoes</b>.
S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape and a Square-shape.
Each of these shapes is formed with four squares. 
The shapes are falling down the board.
The object of the tetris game is to move and rotate the shapes, so that 
they fit as much as possible. If we manage to form a row, the row is destroyed 
and we score. We play the tetris game until we top out.
</p>


<br>
<img src="/img/gui/ironpython/tetrominoes.png" alt="Tetrominoes">
<div class="figure">Figure: Tetrominoes</div>



<h2>The development</h2>

<p>
We do not have images for our tetris game, we draw the tetrominoes using 
the drawing API available in the Winforms library.
Behind every computer game, there is a mathematical model. So it is in tetris. 
</p>

<p>
Some ideas behind the game.
</p>

<ul>
<li>We use <b>Timer</b> to create a game cycle</li>
<li>The tetrominoes are drawn</li>
<li>The shapes move on a square by square basis (not pixel by pixel)</li>
<li>Mathematically a board is a simple list of numbers</li>
</ul>

<br>

<p>
The following example is a modified version of the Tetris game, available 
with PyQt4 installation files.
</p>

<div class="codehead">tetris.py</div>
<pre class="code">
#!/usr/bin/ipy

import clr

clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
clr.AddReference("System")

from System.Windows.Forms import Application, Form, FormBorderStyle
from System.Windows.Forms import UserControl, Keys, Timer, StatusBar
from System.Drawing import Size, Color, SolidBrush, Pen
from System.Drawing.Drawing2D import LineCap
from System.ComponentModel import Container
from System import Random
      
      
      
class Tetrominoes(object):
    NoShape = 0
    ZShape = 1
    SShape = 2
    LineShape = 3
    TShape = 4
    SquareShape = 5
    LShape = 6
    MirroredLShape = 7
        
        
class Board(UserControl):
    BoardWidth = 10
    BoardHeight = 22
    Speed = 200
    ID_TIMER = 1

    def __init__(self):
        self.Text = 'Snake'

        self.components = Container()
        self.isWaitingAfterLine = False
        self.curPiece = Shape()
        self.nextPiece = Shape()
        self.curX = 0
        self.curY = 0
        self.numLinesRemoved = 0
        self.board = []
        
        self.DoubleBuffered = True

        self.isStarted = False
        self.isPaused = False

        self.timer = Timer(self.components)
        self.timer.Enabled = True
        self.timer.Interval = Board.Speed
        self.timer.Tick += self.OnTick

        self.Paint += self.OnPaint
        self.KeyUp += self.OnKeyUp

        self.ClearBoard()


    def ShapeAt(self, x, y):
        return self.board[(y * Board.BoardWidth) + x]

    def SetShapeAt(self, x, y, shape):
        self.board[(y * Board.BoardWidth) + x] = shape

    def SquareWidth(self):
        return self.ClientSize.Width / Board.BoardWidth

    def SquareHeight(self):
        return self.ClientSize.Height / Board.BoardHeight

    def Start(self):
        if self.isPaused:
            return

        self.isStarted = True
        self.isWaitingAfterLine = False
        self.numLinesRemoved = 0
        self.ClearBoard()

        self.NewPiece()


    def Pause(self):
        if not self.isStarted:
            return

        self.isPaused = not self.isPaused        
        statusbar = self.Parent.statusbar

        if self.isPaused:
            self.timer.Stop()
            statusbar.Text = 'paused'
        else:
            self.timer.Start()
            statusbar.Text = str(self.numLinesRemoved)

        self.Refresh()

    def ClearBoard(self):
        for i in range(Board.BoardHeight * Board.BoardWidth):
            self.board.append(Tetrominoes.NoShape)

    def OnPaint(self, event):
     
        g = event.Graphics

        size = self.ClientSize
        boardTop = size.Height - Board.BoardHeight * self.SquareHeight()

        for i in range(Board.BoardHeight):
            for j in range(Board.BoardWidth):
                shape = self.ShapeAt(j, Board.BoardHeight - i - 1)
                if shape != Tetrominoes.NoShape:
                    self.DrawSquare(g,
                        0 + j * self.SquareWidth(),
                        boardTop + i * self.SquareHeight(), shape)

        if self.curPiece.GetShape() != Tetrominoes.NoShape:
            for i in range(4):
                x = self.curX + self.curPiece.x(i)
                y = self.curY - self.curPiece.y(i)
                self.DrawSquare(g, 0 + x * self.SquareWidth(),
                    boardTop + (Board.BoardHeight - y - 1) * self.SquareHeight(),
                    self.curPiece.GetShape())
                    
        g.Dispose()

    def OnKeyUp(self, event): 
        
        if not self.isStarted or self.curPiece.GetShape() == Tetrominoes.NoShape:
            return
            
        key = event.KeyCode

        if key == Keys.P:
            self.Pause()
            return
        
        if self.isPaused:
            return    
        elif key == Keys.Left:
            self.TryMove(self.curPiece, self.curX - 1, self.curY)
        elif key == Keys.Right:
            self.TryMove(self.curPiece, self.curX + 1, self.curY)
        elif key == Keys.Down:
            self.TryMove(self.curPiece.RotatedRight(), self.curX, self.curY)
        elif key == Keys.Up:
            self.TryMove(self.curPiece.RotatedLeft(), self.curX, self.curY)
        elif key == Keys.Space:
            self.DropDown()
        elif key == Keys.D:
            self.OneLineDown()
      

    def OnTick(self, sender, event):

        if self.isWaitingAfterLine:
            self.isWaitingAfterLine = False
            self.NewPiece()
        else:
            self.OneLineDown()


    def DropDown(self):
        newY = self.curY
        while newY > 0:
            if not self.TryMove(self.curPiece, self.curX, newY - 1):
                break
            newY -= 1

        self.PieceDropped()

    def OneLineDown(self):
        if not self.TryMove(self.curPiece, self.curX, self.curY - 1):
            self.PieceDropped()


    def PieceDropped(self):
        for i in range(4):
            x = self.curX + self.curPiece.x(i)
            y = self.curY - self.curPiece.y(i)
            self.SetShapeAt(x, y, self.curPiece.GetShape())

        self.RemoveFullLines()

        if not self.isWaitingAfterLine:
            self.NewPiece()


    def RemoveFullLines(self):
        numFullLines = 0

        statusbar = self.Parent.statusbar

        rowsToRemove = []

        for i in range(Board.BoardHeight):
            n = 0
            for j in range(Board.BoardWidth):
                if not self.ShapeAt(j, i) == Tetrominoes.NoShape:
                    n = n + 1

            if n == 10:
                rowsToRemove.append(i)

        rowsToRemove.reverse()

        for m in rowsToRemove:
            for k in range(m, Board.BoardHeight):
                for l in range(Board.BoardWidth):
                    self.SetShapeAt(l, k, self.ShapeAt(l, k + 1))

        numFullLines = numFullLines + len(rowsToRemove)

        if numFullLines > 0:
            self.numLinesRemoved = self.numLinesRemoved + numFullLines
            statusbar.Text = str(self.numLinesRemoved)
            self.isWaitingAfterLine = True
            self.curPiece.SetShape(Tetrominoes.NoShape)
            self.Refresh()


    def NewPiece(self):
        self.curPiece = self.nextPiece
        statusbar = self.Parent.statusbar
        self.nextPiece.SetRandomShape()
        self.curX = Board.BoardWidth / 2 + 1
        self.curY = Board.BoardHeight - 1 + self.curPiece.MinY()

        if not self.TryMove(self.curPiece, self.curX, self.curY):
            self.curPiece.SetShape(Tetrominoes.NoShape)
            self.timer.Stop()
            self.isStarted = False
            statusbar.Text = 'Game over'

    def TryMove(self, newPiece, newX, newY):
        for i in range(4):
            x = newX + newPiece.x(i)
            y = newY - newPiece.y(i)
            if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
                return False
            if self.ShapeAt(x, y) != Tetrominoes.NoShape:
                return False

        self.curPiece = newPiece
        self.curX = newX
        self.curY = newY
        self.Refresh()
        return True

           
    def DrawSquare(self, g, x, y, shape):
        colors = [ (0, 0, 0), (204, 102, 102), 
            (102, 204, 102), (102, 102, 204), 
            (204, 204, 102), (204, 102, 204), 
            (102, 204, 204), (218, 170, 0) ]
            
        light = [ (0, 0, 0), (248, 159, 171), 
            (121, 252, 121), (121, 121, 252), 
            (252, 252, 121), (252, 121, 252), 
            (121, 252, 252), (252, 198, 0) ]
        
        dark = [ (0, 0, 0), (128, 59, 59), 
            (59, 128, 59), (59, 59, 128), 
            (128, 128, 59), (128, 59, 128), 
            (59, 128, 128), (128, 98, 0) ]   
        
        
        pen = Pen(Color.FromArgb(light[shape][0], light[shape][1],
            light[shape][2]), 1)
        pen.StartCap = LineCap.Flat
        pen.EndCap = LineCap.Flat
        
        g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y)
        g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y)

        darkpen = Pen(Color.FromArgb(dark[shape][0], dark[shape][1],
            dark[shape][2]), 1)
        darkpen.StartCap = LineCap.Flat
        darkpen.EndCap = LineCap.Flat
        
        g.DrawLine(darkpen, x + 1, y + self.SquareHeight() - 1,
            x + self.SquareWidth() - 1, y + self.SquareHeight() - 1)
        g.DrawLine(darkpen, x + self.SquareWidth() - 1, 
            y + self.SquareHeight() - 1, x + self.SquareWidth() - 1, y + 1)
            
        g.FillRectangle(SolidBrush(Color.FromArgb(colors[shape][0], colors[shape][1], 
            colors[shape][2])), x + 1, y + 1, self.SquareWidth() - 1, 
            self.SquareHeight() - 2)
            
        pen.Dispose()
        darkpen.Dispose()



class Shape(object):
    coordsTable = (
        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),
        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),
        ((0, -1),    (0, 0),     (1, 0),     (1, 1)),
        ((0, -1),    (0, 0),     (0, 1),     (0, 2)),
        ((-1, 0),    (0, 0),     (1, 0),     (0, 1)),
        ((0, 0),     (1, 0),     (0, 1),     (1, 1)),
        ((-1, -1),   (0, -1),    (0, 0),     (0, 1)),
        ((1, -1),    (0, -1),    (0, 0),     (0, 1))
    )

    def __init__(self):
        self.coords = [[0,0] for i in range(4)]
        self.pieceShape = Tetrominoes.NoShape

        self.SetShape(Tetrominoes.NoShape)

    def GetShape(self):
        return self.pieceShape

    def SetShape(self, shape):
        table = Shape.coordsTable[shape]
        for i in range(4):
            for j in range(2):
                self.coords[i][j] = table[i][j]

        self.pieceShape = shape

    def SetRandomShape(self):
        rand = Random()
        self.SetShape(rand.Next(1, 7))

    def x(self, index):
        return self.coords[index][0]

    def y(self, index):
        return self.coords[index][1]

    def SetX(self, index, x):
        self.coords[index][0] = x

    def SetY(self, index, y):
        self.coords[index][1] = y

    def MaxX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = max(m, self.coords[i][0])

        return m

    def MinY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = min(m, self.coords[i][1])

        return m

    def RotatedLeft(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.SetX(i, self.y(i))
            result.SetY(i, -self.x(i))

        return result

    def RotatedRight(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.SetX(i, -self.y(i))
            result.SetY(i, self.x(i))

        return result   

        
class IForm(Form):

    def __init__(self):
        self.Text = 'Tetris'
        self.Width = 200
        self.Height = 430
        self.FormBorderStyle = FormBorderStyle.FixedSingle
        board = Board()
        board.Width = 195
        board.Height = 380
        self.Controls.Add(board)

        self.statusbar = StatusBar()
        self.statusbar.Parent = self
        self.statusbar.Text = 'Ready'
        board.Start()
        self.CenterToScreen()
        
        
Application.Run(IForm())
</pre>



<p>
I have simplified the game a bit, so that it is easier to understand. 
The game starts immediately, after it is launched.
We can pause the game by pressing the p key. The space key will drop 
the tetris piece immediately to the bottom. The d key will drop the 
piece one line down. (It can be used to speed up the falling a bit.)
The game goes at constant speed, no acceleration is implemented. 
The score is the number of lines, that we have removed.
</p>

<pre class="explanation">
 class Tetrominoes(object):
     NoShape = 0
     ZShape = 1
     SShape = 2
     LineShape = 3
     TShape = 4
     SquareShape = 5
     LShape = 6
     MirroredLShape = 7
</pre>

<p>
There are seven different types of tetrominoes. 
</p>


<pre class="explanation">
 ...
 self.curX = 0
 self.curY = 0
 self.numLinesRemoved = 0
 self.board = []
 ...
</pre>

<p>
Before we start the game cycle, we initialize some important variables. 
The <b class="keyword">self.board</b> variable is a list of <b class="keyword">Tetrominoes</b>.
It represents the position of various shapes and remains of the shapes on the board.
</p>

<pre class="explanation">
 def ClearBoard(self):
     for i in range(Board.BoardHeight * Board.BoardWidth):
         self.board.append(Tetrominoes.NoShape)
</pre>

<p>
The <b class="keyword">ClearBoard()</b> method clears the board. 
It fills the <b class="keyword">self.board</b> variable with 
<b class="keyword">Tetrominoes.NoShape</b> values.
</p>

<p>
Painting in the tetris game is done in the <b class="keyword">OnPaint()</b>
method.
</p>

<pre class="explanation">
 for i in range(Board.BoardHeight):
     for j in range(Board.BoardWidth):
         shape = self.shapeAt(j, Board.BoardHeight - i - 1)
         if shape != Tetrominoes.NoShape:
             self.drawSquare(g,
                 0 + j * self.squareWidth(),
                 boardTop + i * self.squareHeight(), shape)
</pre>

<p>
The painting of the game is divided into two steps.
In the first step, we draw all the shapes, or remains of the shapes, that
have been dropped to the bottom of the board. 
All the squares are rememberd in the <b class="keyword">self.board</b> list.
We access it using the <b class="keyword">ShapeAt()</b> method.
</p>

<pre class="explanation">
 if self.curPiece.shape() != Tetrominoes.NoShape:
     for i in range(4):
         x = self.curX + self.curPiece.x(i)
         y = self.curY - self.curPiece.y(i)
         self.drawSquare(g, 0 + x * self.squareWidth(),
             boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
             self.curPiece.shape())
</pre>

<p>
The next step is drawing of the actual piece, that is falling down.
</p>

<p>
In the <b class="keyword">OnKeyUp()</b> method we check for pressed keys. 
</p>

<pre class="explanation">
 elif key == Keys.Left:
     self.tryMove(self.curPiece, self.curX - 1, self.curY)
</pre>

<p>
If we press the left arrow key, we try to move
the piece to the left. We say try, because the piece might not be able to move. 
</p>

<p>
In the <b class="keyword">TryMove()</b> method we try to move our shapes.
If we cannot move the piece, we return False.
</p>

<pre class="explanation">
 for i in range(4):
     x = newX + newPiece.x(i)
     y = newY - newPiece.y(i)
     if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
         return False
     if self.ShapeAt(x, y) != Tetrominoes.NoShape:
         return False
</pre>

<p>
If the shape is at the edge of the board or is adjacent to some
other piece, we return False. 
</p>

<pre class="explanation">
 self.curPiece = newPiece
 self.curX = newX
 self.curY = newY
 self.Refresh()
 return True
</pre>

<p>
Otherwise we place the current falling piece to a 
new position and return True.
</p>


<pre class="explanation">
 def OnTick(self, sender, event):

    if self.isWaitingAfterLine:
        self.isWaitingAfterLine = False
        self.NewPiece()
    else:
        self.OneLineDown()
</pre>

<p>
In the <b class="keyword">OnTick()</b> method we either create a new piece,
after the previous one hit the bottom, 
or we move a falling piece one line down. 
</p>

<p>
If the piece hits the bottom, we call the <b class="keyword">RemoveFullLines()</b> method. 
First we find out all full lines. 
</p>

<pre class="explanation">
  rowsToRemove = []
  for i in range(Board.BoardHeight):
      n = 0
      for j in range(Board.BoardWidth):
          if not self.ShapeAt(j, i) == Tetrominoes.NoShape:
              n = n + 1
      if n == 10:
          rowsToRemove.append(i)
</pre>

<p>
We cycle throught the board. A row can have ten pieces of shapes. 
If the row is full, e.g. n is equal to 10, we store the line
number for later removal.
</p>

<pre class="explanation">
 rowsToRemove.reverse()
       
 for m in rowsToRemove:
     for k in range(m, Board.BoardHeight):
         for l in range(Board.BoardWidth):
             self.SetShapeAt(l, k, self.ShapeAt(l, k + 1))
</pre>

<p>
These code lines remove the full lines. We reverse the order
of the <b class="keyword">rowsToRemove</b> list so that we begin
with the bottom most full
line. What we do is remove a full line by placing all lines
about it one line down. This happens for all full lines
In our case we use a <b>naive gravity</b>. 
This means, that the pieces may be lef floating above empty gaps.
</p>


<pre class="explanation">
 def NewPiece(self):
     self.curPiece = self.nextPiece
     statusbar = self.Parent.statusbar
     self.nextPiece.SetRandomShape()
     self.curX = Board.BoardWidth / 2 + 1
     self.curY = Board.BoardHeight - 1 + self.curPiece.MinY()

     if not self.TryMove(self.curPiece, self.curX, self.curY):
         self.curPiece.SetShape(Tetrominoes.NoShape)
         self.timer.Stop()
         self.isStarted = False
         statusbar.Text = 'Game over'
</pre>

<p>
The <b class="keyword">NewPiece()</b> method creates randomly a new tetris piece.
If the piece cannot go into it's initial position, e.g. the <b class="keyword">TryMove()</b>
method returns False, the game is over. 
</p>

<pre class="explanation">
 colors = [ (0, 0, 0), (204, 102, 102), 
     ... ]
    
 light = [ (0, 0, 0), (248, 159, 171), 
     ... ]

 dark = [ (0, 0, 0), (128, 59, 59), 
     ... ]   
</pre>

<p>
There are three lists of colors. The <b class="keyword">colors</b> list stores
color values for the fills of the squares. Each of seven pieces has its own
color. The <b class="keyword">light</b> and the <b class="keyword">dark</b>
store colors for lines, that will make the square look 3D. These colors 
are the same, just are lighter and darker. We will draw
two lines with light color to the top and left sides of the squares and 
two lines with darker color to the right and bottom sides.
</p>

<pre class="explanation">
 g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y)
 g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y)
</pre>

<p>
These two lines draw the light lines for a square. 
</p>

<br><br>
<p>
The <b>Shape</b> class saves information about the tetris piece. 
</p>


<pre class="explanation">
 self.coords = [[0,0] for i in range(4)]
</pre>

<p>
Upon creation we create an empty coordinates list. 
The list will save the coordinates of the tetris piece.
For example, these tuples (0, -1), (0, 0), (1, 0), (1, 1) 
represent a rotated S-shape. The following diagram illustrates
the shape.
</p>


<br>
<img src="/img/gui/ironpython/coordinates.png" alt="Coordinates">
<div class="figure">Figure: Coordinates</div>

<p>
When we draw the current falling piece, we draw it at <b class="keyword">self.curX</b>, 
<b class="keyword">self.curY</b> position.
Then we look at the coordinates table and draw all the four squares. 
</p>

<p>
The <b class="keyword">RotateLeft()</b> method rotates a piece to the left.
</p>

<pre class="explanation">
 if self.pieceShape == Tetrominoes.SquareShape:
     return self
</pre>

<p>
If we have the <b class="keyword">Tetrominoes.SquareShape</b>
piece, we do nothing. This shape is always the same.
</p>

<pre class="explanation">
 result = Shape()
 result.pieceShape = self.pieceShape
 for i in range(4):
     result.SetX(i, self.y(i))
     result.SetY(i, -self.x(i))

 return result
</pre>

<p>
In other cases, we change coordinates of the piece.
To understand this code, look at the above figure.
</p>

<br>
<img src="/img/gui/ironpython/tetris.png" alt="Tetris">
<div class="figure">Figure: Tetris</div>

<hr class="btm">

<p>
This was Tetris game in IronPython Winforms. 
</p>


<div class="center">
<script type="text/javascript"><!--
google_ad_client = "pub-9706709751191532";
/* horizontal */
google_ad_slot = "1734478269";
google_ad_width = 468;
google_ad_height = 60;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>
<br>


<div class="botNav, center">
<span class="botNavItem"><a href="/">Home</a></span> ‡ <span class="botNavItem"><a href="..">Contents</a></span> ‡ 
<span class="botNavItem"><a href="#">Top of Page</a></span>
</div>


<div class="footer">
<div class="signature">
<a href="/">ZetCode</a> last modified January 31, 2009  <span class="copyright">&copy; 2007 - 2012 Jan Bodnar</span>
</div>
</div>

</div> <!-- content -->

</div> <!-- container -->

</body>
</html>


